GEM5实际上是一个时钟驱动的离散事件模拟器
ISA构建与解析
gem5 指令集描述语言是一种专门为生成 gem5 所需类定义和解码函数而设计的自定义语言。
该语言的正式语法嵌入在解析器的“yacc”部分(在 isa_parser.py 中查找以 p_开头的函数)。解析器的第二个主要组件处理类似 C 的代码规范以提取指令特性;这方面在代码解析部分中介绍。
在最高层次上,指令集描述文件分为两部分:声明部分和解码部分。解码部分指定了解码器的结构,并定义了解码器返回的特定指令。声明部分定义了支持解码器所需的全局信息(类、指令格式、模板等)。更详细文档
decode
decode bitfield {
format XX {
0x00: xx({{ format
def format FormatName(arg1, arg2) {{
[code omitted]
#header_output -> decoder.hh
#decoder_output -> decoder.cc
#decode_block -> decode case语句
#exec_output -> generic_put_exec.cc
}}; template
def template LoadStoreDeclare {{
class xx : public xxx
{
[code omitted]
};
}}; output
output header/decoder/exec {{ //header_output,decoder_output,exec_output
[code omitted]
}}; let 定义共享的python数据结构和函数,用于指令格式中使用
let {{
[code omitted]
}}; bitfield
def bitfield xx <0:0>; operand
def operand_types/operands 从decoder.isa中调用format,生成四部分输出:
- header_output
- decoder_output
- exec_output
- decode_block
分别对应:
header_output -> decoder-ns.hh.inc 指令class实现
decoder_output -> decoder-ns.cc.inc 指令class构造函数实现,以及指令class声明
exec_output -> exec-ns.cc.inc 指令的execute、initiateAcc、completeAcc函数实现
decode_block -> decode-method.cc.inc 构造decoder函数,根据解码构造指令class
inst-constrs.cc
#include "decoder-g.cc.inc"
#include "decoder-g.hh.inc"
#include "decoder-ns.hh.inc"
#include "decoder-ns.cc.inc"
decoder.cc
#include "decoder-g.cc.inc"
#include "decoder-g.hh.inc"
#include "decoder-ns.hh.inc"
#include "decode-method.cc.inc"
generic_cpu_exec.cc
#include "exec-g.cc.inc"
#include "decoder-g.hh.inc"
#include "decoder-ns.hh.inc"
#include "exec-ns.cc.inc"
结构体&流程
Fetch2
- ForwardInstData
- ForwardLineData
- BranchData
- Fetch2ThreadInfo
ISA::readMiscReg => BaseISA::ThreadContext::PCState::VTYPE(vector state)
// xc
ExecContext
- ThreadContext // tc
- BaseCPU
-
- BaseMMU
- BaseISA
- ISA
- CSRData
- miscRegFile
- InstDecoder
- ExtMatchInst
- Process
- Status
- PCState
- VTYPE
ThreadContext
|
V
StaticInst::ExtMachInst
MinorDynInst::StaticInst
ThreadState
- ThreadContext::Status
- BaseCPU
- Process
- ContextID
- ThreadID
SimpleThread : ThreadState,ThreadContext
- BaseISA
- PCStateBase
- BaseHTMCheckpoint
- PCEventQueue
- EventQueue
- System
- BaseMMU
- InstDecoder
RiscvProcess64::initState() -> Process::initState(); -> tc->SimpleThread::activate(); -> baseCpu->activateContext(_threadId); -> MinorCPU::activateContext(ThreadID thread_id)
-> wakeupOnEvent -> pipeline->start();
-> (只有一次)pipeline wakeupFetch -> wakeupFetch -> cpu wakeupOnEvent -> ticked_object start -> pipeline->start();
MinorCPU::startup() -> pipeline wakeupFetch -> wakeupFetch -> ...
event processClockEvent -> evaluate
thread activate -> cpu activateContext -> pipeline wakeupFetch -> wakeupFetch -> cpu wakeupOnEvent -> ticked_object start ->
event processClockEvent -> evaluate execute,initiateAcc,completeAcc
下面这三个钩子是 gem5 指令语义里对访存类指令的三种执行路径(或阶段)。一句话版:
-
execute():单阶段、阻塞式执行(一次函数调用就把指令所有副作用都做完,包含访存)。给 AtomicSimpleCPU、功能仿真(functional) 等“零/常数时延”路径用。 -
initiateAcc():分阶段的第 1 阶段——只发起访存请求(构造 Request/Packet,送到内存系统),不做依赖返回数据的副作用。给 TimingSimpleCPU / O3 / Minor 等“时序/分阶段”路径用。 -
completeAcc():分阶段的第 2 阶段——在内存响应返回后被回调,消化返回数据并提交副作用(写目的寄存器、更新标志位、提交 store 成功/失败等)。
什么时候需要实现?
只跑 Atomic / 功能模式
- 指令不访问内存:只实现
execute()即可。 - 指令访问内存但你只打算用 AtomicSimpleCPU 或功能模式:实现
execute()就能工作(它内部会用原子/功能访问把数据一次性读/写好)。
要支持 Timing / O3 / Minor(真实时序)
- 凡是会访问内存的指令(load/store、原子指令、LL/SC、向量 gather/scatter、访存型自定义扩展、设备 MMIO 等),你必须实现成对的
initiateAcc()(发请求) +completeAcc()(收响应并提交副作用)。否则:
- 时序 CPU 无法感知访存时延,会当作 0 周期完成;
- 乱序/重排序、依赖、异常/重放等都会不正确。
gem5 的解码器/代码生成通常会同时给出三套路径的空壳。你在做自定义指令或改语义时,需要把访存逻辑分别填在 execute()(原子/功能)和 initiateAcc()+completeAcc()(时序)两套里。
三者的职责边界(重要!)
execute(xc, trace)
- 可阻塞:直接调用
xc->readMem/ writeMem(原子/功能访问),拿到数据后当场写目的寄存器 / 写内存 / 更新标志位。 - 适合:AtomicSimpleCPU,功能访问(调试/校验)。
initiateAcc(xc, trace)
- 只做两件事:
- 计算 EA/大小/字节使能、准备数据缓冲(store/AMO),
- 通过
xc发出内存请求(通常隐藏 TLB/端口发送)。
- 不要在这里修改任何依赖返回数据的架构状态(例如 load 的目的寄存器、比较结果标志等)。
- 对于分段/多拍(如 vector、按 64B 一组)可以发多次请求,并在动态指令对象里记录 outstanding 计数/状态。
completeAcc(pkt, xc, trace)
- 在内存系统回包时被 CPU 回调。
- 这里读取返回数据、写目的寄存器、更新标志位、完成 store-conditional 判定、清理 outstanding、标注指令完成等。
- 多拍/多分段时,通常等最后一个响应到达再真正“提交”。
下面结合 gem5 的 MinorCPU 实现(src/cpu/minor/execute.cc)把三类回调的触发时机讲清楚:
1) execute(...) —— 非内存类指令在提交(commit)时调用
- 当待提交的是非内存指令(ALU、分支、CSR 等),
Execute::commit()会直接构造执行上下文ExecContext并调用staticInst->execute(&context, trace)完成语义更新、设置谓词、处理可能的 fault,然后做分支处理与统计记账。参见commitInst里调用staticInst->execute(...)的代码。 Doxygen Gem5
(注:若是内存屏障且 store buffer 没位,或可挂起的 quiesce 指令等,会被延后,不进入 execute 分支。Doxygen Gem5)
2) initiateAcc(...) —— 内存指令“发起访问”时调用
- 内存指令在功能单元(FU)里先计算好有效地址(EA)。当它到达可发起内存访问的时刻(允许早发
allowEarlyMemIssue时可在到队头前发,否则到队头再发),Execute::executeMemRefInst()会调用staticInst->initiateAcc(&context, trace)去构造并送出访存请求(进入 LSQ/内存系统),可能产生翻译 fault(TLB fault)等。相关调用见: Doxygen Gem5- 代码中:
init_fault = inst->staticInst->initiateAcc(&context, inst->traceData);(并对 translation fault 有特殊处理)。 Doxygen Gem5 - 若启用“早发”(early memory issue)且发生 fault,会被记录下来,等真正轮到提交时再统一处理。 Doxygen Gem5
- 代码中:
3) completeAcc(...) —— 内存指令“收到响应/完成访问”时调用
- 当内存系统返回响应(或访问失败/被抑制)时,
handleMemResponse路径会调用staticInst->completeAcc(packet, &context, trace)完成这条内存指令的语义收尾(例如把 load 的数据写回目的寄存器、完成 store 的提交、更新标志位等;如果packet为nullptr表示失败路径/谓词不通过也走这里)。见:-
inst->staticInst->completeAcc(packet, &context, inst->traceData);,以及失败时completeAcc(nullptr, ...)。 Doxygen Gem5
-
一张流程小抄(内存指令)
- FU 阶段:算 EA。
- 发起内存:满足条件时调用
initiateAcc(...),请求进入 LSQ/内存系统(支持 early-issue 时可提前发)。 Doxygen Gem5+1 - 返回并完成:收到响应后调用
completeAcc(...),做寄存器/状态更新并使这条内存指令可提交。 Doxygen Gem5 - 非内存:不走上面两步,直接在
commit中调用execute(...)。 Doxygen Gem5
关键词定位:
completeAcc调用:execute.cc 中处理内存响应的位置。Doxygen Gem5initiateAcc调用:execute 内的内存发起逻辑。Doxygen Gem5- 非内存走
execute(...):commitInst中非内存路径。Doxygen Gem5
如果你要在自定义指令里接入访存:
- 语义拆分到
initiateAcc(发请求)和completeAcc(拿响应写回/收尾); - 非访存语义放在
execute。 - 打开/关闭
allowEarlyMemIssue会影响“先发访存、后到队头提交”的行为。 Doxygen Gem5
gem5执行流程
在 gem5 中,m5.main() 函数的执行过程从 Python 开始,然后通过 pybind11 嵌入 Python 到 C++,最终进入 gem5 的核心模拟代码。这一流程通过 pybind11 提供的接口,使得 Python 代码可以调用 C++ 中的实现部分。
libc.so.6!__pthread_kill_implementation(int no_tid, int signo, pthread_t threadid) (pthread_kill.c:44)
libc.so.6!__pthread_kill_internal(int signo, pthread_t threadid) (pthread_kill.c:78)
libc.so.6!__GI___pthread_kill(pthread_t threadid, int signo) (pthread_kill.c:89)
libc.so.6!__GI_raise(int sig) (raise.c:26)
libc.so.6!__GI_abort() (abort.c:79)
gem5::Logger::exit_helper(gem5::Logger * const this) (/data/user61/work_jan/gem5-a4e/src/base/logging.hh:124)
gem5::TCM::CPUSidePort::accessFunctional(gem5::TCM::CPUSidePort * const this, gem5::PacketPtr pkt) (/data/user61/work_jan/gem5-a4e/src/dev/tcm.cc:488)
gem5::TCM::CPUSidePort::recvFunctional(gem5::TCM::CPUSidePort * const this, gem5::PacketPtr pkt) (/data/user61/work_jan/gem5-a4e/src/dev/tcm.cc:204)
gem5::FunctionalRequestProtocol::send(const gem5::FunctionalRequestProtocol * const this, gem5::FunctionalResponseProtocol * peer, gem5::PacketPtr pkt) (/data/user61/work_jan/gem5-a4e/src/mem/protocol/functional.cc:53)
gem5::RequestPort::sendFunctional(const gem5::RequestPort * const this, gem5::PacketPtr pkt) (/data/user61/work_jan/gem5-a4e/src/mem/port.hh:583)
operator()(const struct {...} * const __closure, gem5::PacketPtr pkt) (/data/user61/work_jan/gem5-a4e/src/mem/port_proxy.cc:53)
std::__invoke_impl<void, gem5::PortProxy::PortProxy(const gem5::RequestPort&, gem5::Addr)::<lambda(gem5::PacketPtr)>&, gem5::Packet*>(std::__invoke_other, struct {...} &)(struct {...} & __f) (/usr/include/c++/11/bits/invoke.h:61)
std::__invoke_r<void, gem5::PortProxy::PortProxy(const gem5::RequestPort&, gem5::Addr)::<lambda(gem5::PacketPtr)>&, gem5::Packet*>(struct {...} &)(struct {...} & __fn) (/usr/include/c++/11/bits/invoke.h:111)
std::_Function_handler<void(gem5::Packet*), gem5::PortProxy::PortProxy(const gem5::RequestPort&, gem5::Addr)::<lambda(gem5::PacketPtr)> >::_M_invoke(const std::_Any_data &, gem5::Packet *&&)(const std::_Any_data & __functor, __args#0) (/usr/include/c++/11/bits/std_function.h:290)
std::function<void (gem5::Packet*)>::operator()(gem5::Packet*) const(const std::function<void(gem5::Packet*)> * const this, __args#0) (/usr/include/c++/11/bits/std_function.h:590)
gem5::PortProxy::writeBlobPhys(const gem5::PortProxy * const this, gem5::Addr addr, gem5::Request::Flags flags, const void * p, uint64_t size) (/data/user61/work_jan/gem5-a4e/src/mem/port_proxy.cc:96)
gem5::PortProxy::tryWriteBlob(const gem5::PortProxy * const this, gem5::Addr addr, const void * p, uint64_t size) (/data/user61/work_jan/gem5-a4e/src/mem/port_proxy.hh:159)
gem5::PortProxy::writeBlob(const gem5::PortProxy * const this, gem5::Addr addr, const void * p, uint64_t size) (/data/user61/work_jan/gem5-a4e/src/mem/port_proxy.hh:194)
gem5::loader::MemoryImage::writeSegment(const gem5::loader::MemoryImage * const this, const gem5::loader::MemoryImage::Segment & seg, const gem5::PortProxy & proxy) (/data/user61/work_jan/gem5-a4e/src/base/loader/memory_image.cc:43)
gem5::loader::MemoryImage::write(const gem5::loader::MemoryImage * const this, const gem5::PortProxy & proxy) (/data/user61/work_jan/gem5-a4e/src/base/loader/memory_image.cc:56)
gem5::Process::initState(gem5::Process * const this) (/data/user61/work_jan/gem5-a4e/src/sim/process.cc:375)
gem5::RiscvProcess64::initState(gem5::RiscvProcess64 * const this) (/data/user61/work_jan/gem5-a4e/src/arch/riscv/process.cc:102)
pybind11::cpp_function::cpp_function<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}::operator()(gem5::SimObject*) const(const struct {...} * const __closure, gem5::SimObject * c) (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/pybind11.h:110)
pybind11::detail::argument_loader<gem5::SimObject*>::call_impl<void, pybind11::cpp_function::cpp_function<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}&, 0ul, pybind11::detail::void_type>(pybind11::cpp_function::cpp_function<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}&, std::integer_sequence<unsigned long, 0ul>, pybind11::detail::void_type&&) &&(pybind11::detail::argument_loader<gem5::SimObject*> * const this, struct {...} & f) (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/cast.h:1443)
pybind11::detail::argument_loader<gem5::SimObject*>::call<void, pybind11::detail::void_type, pybind11::cpp_function::cpp_function<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}&>(pybind11::cpp_function::cpp_function<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}&) &&(pybind11::detail::argument_loader<gem5::SimObject*> * const this, struct {...} & f) (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/cast.h:1417)
pybind11::cpp_function::initialize<pybind11::cpp_function::initialize<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}, void, gem5::SimObject*, pybind11::name, pybind11::is_method, pybind11::sibling>(pybind11::cpp_function::initialize<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}&&, void (*)(gem5::SimObject*), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(pybind11::detail::function_call&)#3}::operator()(pybind11::detail::function_call&) const(const struct {...} * const __closure, pybind11::detail::function_call & call) (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/pybind11.h:248)
pybind11::cpp_function::initialize<pybind11::cpp_function::initialize<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}, void, gem5::SimObject*, pybind11::name, pybind11::is_method, pybind11::sibling>(pybind11::cpp_function::initialize<void, gem5::SimObject, , pybind11::name, pybind11::is_method, pybind11::sibling>(void (gem5::SimObject::*)(), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(gem5::SimObject*)#1}&&, void (*)(gem5::SimObject*), pybind11::name const&, pybind11::is_method const&, pybind11::sibling const&)::{lambda(pybind11::detail::function_call&)#3}::_FUN(pybind11::detail::function_call&)() (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/pybind11.h:223)
pybind11::cpp_function::dispatcher(PyObject * self, PyObject * args_in, PyObject * kwargs_in) (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/pybind11.h:939)
libpython3.10.so.1.0![Unknown/Just-In-Time compiled code] (Unknown Source:0)
libpython3.10.so.1.0!_PyObject_MakeTpCall (Unknown Source:0)
libpython3.10.so.1.0![Unknown/Just-In-Time compiled code] (Unknown Source:0)
libpython3.10.so.1.0!_PyEval_EvalFrameDefault (Unknown Source:0)
libpython3.10.so.1.0![Unknown/Just-In-Time compiled code] (Unknown Source:0)
libpython3.10.so.1.0!_PyEval_EvalFrameDefault (Unknown Source:0)
libpython3.10.so.1.0![Unknown/Just-In-Time compiled code] (Unknown Source:0)
libpython3.10.so.1.0!PyEval_EvalCode (Unknown Source:0)
libpython3.10.so.1.0![Unknown/Just-In-Time compiled code] (Unknown Source:0)
libpython3.10.so.1.0!_PyEval_EvalFrameDefault (Unknown Source:0)
libpython3.10.so.1.0![Unknown/Just-In-Time compiled code] (Unknown Source:0)
pybind11::detail::simple_collector<(pybind11::return_value_policy)1>::call(const pybind11::detail::simple_collector<(pybind11::return_value_policy)1> * const this, PyObject * ptr) (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/cast.h:1465)
pybind11::detail::object_api<pybind11::detail::accessor<pybind11::detail::accessor_policies::str_attr> >::operator()<(pybind11::return_value_policy)1>(const pybind11::detail::object_api<pybind11::detail::accessor<pybind11::detail::accessor_policies::str_attr> > * const this) (/data/user61/work_jan/gem5-a4e/ext/pybind11/include/pybind11/cast.h:1635)
main(int argc, char ** argv) (/data/user61/work_jan/gem5-a4e/src/sim/main.cc:87) pybind11 cpp_function执行流程
我们可以将 pybind11::cpp_function 的工作原理拆解为以下几个关键步骤:
1. 触发阶段:Python 虚拟机 (VM) 到 C API
调用栈底部(main.cc 向上)显示了 Python 解释器的运行过程:
-
PyEval_EvalFrameDefault: 这是 Python 解释器的主循环,正在执行编译后的 Python 字节码。 -
_PyObject_MakeTpCall: 当 Python 脚本调用system.process.initState()时,解释器识别出这是一个被封装的 C++ 对象方法,于是通过 Python C API 发起调用。
2. 桥接阶段:cpp_function::dispatcher (最关键的一环)
图中 pybind11.h:939 附近的 dispatcher 是整个 pybind11 的“交通枢纽”:
- 职责:它是一个符合 Python 调用约定的
static函数。它接收原始的PyObject*参数。 - 逻辑:它会查找该函数对应的
function_record(存储了 C++ 函数指针、参数类型等信息)。
3. 类型转换阶段:argument_loader
接下来的几行 argument_loader 展示了 pybind11 的元编程魔法:
-
argument_loader<...>::call: 它像一个“翻译官”,利用 C++ 模板遍历 Python 传入的参数元组(tuple)。 - Type Casting: 它会为每个参数调用
type_caster。例如,将 Python 的字符串转为std::string,或将 Python 对象引用转为 gem5 内部的SimObject*指针。 - 图中体现:你看到调用栈里有多个
initialize和call_impl的层级,这正是 pybind11 在尝试匹配正确的函数重载(Overload Resolution)。
4. 执行阶段:进入 gem5 核心逻辑
当参数被全部转换并压入 C++ 栈后,cpp_function 会发起最后一击:
- 调用 Lambda/绑定函数:在图中对应
pybind11::cpp_function::cpp_function<void, gem5::SimObject, ...>这一行,它解包参数并调用我们在gem5导出源码中注册的那个函数。 - 业务逻辑执行:
-
gem5::RiscvProcess64::initState(): 进入特定架构的初始化。 -
gem5::Process::initState(): 基类初始化。 -
gem5::PortProxy::writeBlobPhys(): 最终落到 gem5 内存系统的操作上。
-
总结工作原理图解
核心公式: Python Call → rightarrow Dispatcher → rightarrow Type Casting → rightarrow C++ → Implementation
以下是 m5.main() 函数的执行流程,以及它如何进入 C++ 代码的详细步骤:
1. C++ 初始化 Python 解释器
在 gem5 的 C++ 代码中,首先通过 pybind11 初始化 Python 解释器(如下代码所示):
py::scoped_interpreter guard(true, argc, argv); - 这行代码创建了一个
py::scoped_interpreter对象,初始化 Python 环境并设置命令行参数。 - 这样,
gem5的 C++ 代码可以加载并运行 Python 脚本。
2. 导入并执行 Python 模块中的 main 函数
在 C++ 代码中,Python 模块 m5 被导入并调用 main() 函数:
auto importer = py::module_::import("importer");
importer.attr("install")();
py::module_::import("m5").attr("main")(); -
py::module_::import("m5")导入m5Python 模块。 -
attr("main")()获取m5模块中的main函数并立即执行。 - 此时,
m5.main()开始执行gem5的 Python 代码。
3. Python 代码中的 m5.main() 初始化模拟
m5.main() 是 Python 层的主要入口,位于 src/python/m5/main.py 文件中。它负责设置仿真参数并启动仿真核心:
def main():
# 设置仿真参数和配置
# 这里进行各种组件的初始化,例如 CPU、内存、缓存等
# 进入模拟核心并执行
m5.simulate() -
main()函数通常会解析用户配置,创建并连接各个模拟组件(如 CPU、内存、缓存等)。 - 关键调用是
m5.simulate(),它启动了gem5的 C++ 仿真核心。
4. 加载并执行用户的配置脚本
if options.m:
# 运行指定的 Python 模块
sys.argv = [options.m[0]] + options.m[1]
runpy.run_module(options.m[0], run_name="__m5_main__")
else:
# 运行指定的 Python 脚本文件
if options.c:
filedata = options.c[0]
filecode = compile(filedata, "<string>", "exec")
sys.argv = ["-c"] + options.c[1]
scope = {"__name__": "__m5_main__"}
else:
filename = sys.argv[0]
with open(filename, "rb") as fd:
filedata = fd.read().decode("utf-8")
filecode = compile(filedata, filename, "exec")
scope = {"__file__": filename, "__name__": "__m5_main__"}
exec(filecode, scope) -
options.m和options.c是用户通过命令行传递的参数,分别指向用户自定义的 Python 模块或脚本文件。 -
runpy.run_module()加载并运行模块,exec()执行脚本文件。 - 用户的配置脚本通常会包含
m5.simulate()或类似的调用,以启动仿真。
5. 在用户配置脚本中调用 m5.simulate()
- 用户配置脚本负责定义仿真参数、构建系统组件(如 CPU、内存、缓存等),并最终调用
m5.simulate()来启动仿真。 - 例如,一个典型的
gem5配置脚本可能如下:import m5 from m5.objects import System, Root # 设置系统配置 system = System() # 配置 CPU、内存、缓存等 # ... # 设置根对象并开始仿真 root = Root(full_system=False, system=system) m5.instantiate() # 开始仿真 m5.simulate() -
m5.simulate()是进入gem5C++ 仿真核心的关键入口。
在 m5 Python 模块中,simulate() 实际上是一个由 pybind11 绑定到 C++ 的函数,它启动了 gem5 的模拟主循环。其定义通常如下:
# src/python/m5/simulate.py
def simulate(*args, **kwargs):
sim_out = _m5.event.simulate(*args, **kwargs) -
_m5是由pybind11生成的 C++ 扩展模块,通过它可以直接调用 C++ 层的simulate函数。 -
_m5.event.simulate(*args, **kwargs)调用pybind11/event.cc中的pybind_init_event进入了gem5的 C++ 仿真核心代码,执行 C++ 模拟循环。
6. 进入 C++ 的 simulate 函数
在 gem5 的 C++ 代码中,simulate 是仿真主循环的入口点,位于 src/sim/simulate.cc 中:
void simulate(Tick num_ticks) {
// 主循环,处理事件队列,执行仿真步骤
while (eventQueue->getNextEventTime() <= num_ticks) {
eventQueue->serviceOne();
}
} -
simulate函数会进入一个循环,从事件队列中逐个取出事件并执行,直到仿真结束。 - 在事件驱动的仿真循环中,
gem5根据预设的仿真配置执行各个硬件模块的操作。
7. 仿真结束并返回 Python 层
- 仿真结束后,C++ 中的
simulate函数返回。 - 通过
pybind11,控制权回到 Python 的m5.simulate()函数。 - 最后,
m5.main()执行结束,Python 层完成仿真流程,程序退出。
总结
- 初始化 Python 解释器:C++ 通过
pybind11初始化 Python 解释器。 - 调用 Python
m5.main():C++ 调用m5模块的main()函数,加载并配置模拟环境。 - 启动仿真核心:
m5.main()调用m5.simulate(),启动 C++ 层的simulate函数。 - 进入 C++ 仿真循环:
simulate函数在 C++ 中执行仿真循环,直到所有事件完成。 - 返回控制:仿真结束后,返回 Python,程序结束。
通过 pybind11,Python 和 C++ 层能够顺畅交互,实现了 gem5 仿真启动、控制和事件驱动的全过程。
1. create() (构造函数阶段)
这是最开始的阶段。Python 层的参数(Params)被传递给 C++。
作用: 调用 C++ 类的构造函数。
主要任务: 分配内存,保存配置参数,初始化成员变量。此时对象之间还没有建立真正的连接(如 Port 还没连上)。
2. regStats() (统计信息注册)
在对象创建后,系统会立即调用这个函数。
作用: 让每个 SimObject 声明它将要记录哪些数据(如 Scalar, Formula, Vector 等)。
重要性: 只有在这里注册过的统计量,最后才会在 stats.txt 中出现。
3. init() (拓扑连接阶段) —— 非常关键
在所有对象都 create 完毕后调用。
作用: 解决对象之间的依赖关系。
主要任务: * Port 绑定:确保 Master Port 和 Slave Port 成功连接。
引用获取:如果一个对象需要指向另一个对象的指针(例如 Cache 需要指向 Memory Controller),通常在这一步完成。
注意: 此时你不能调度事件(Event),因为系统还没准备好。
4. regProbePoints() / regProbeListeners() (探针注册)
这是 gem5 的观测系统。
作用: 允许外部组件“监听”该对象的内部行为(例如监听指令提交、Cache Miss 等),而不改变其原始逻辑。
5. initState() (初态设置)
这是你提到的那个函数。
作用: 仅在**非恢复(Non-checkpoint)**运行模式下执行。
主要任务: 设置内存中的初始数据、初始化 CPU 寄存器状态、加载引导程序(Bootloader)等。
6. loadState() (状态加载)
作用: 与 initState() 互斥。如果你是从 Checkpoint(检查点)恢复运行的,系统会调用这个函数把磁盘上的状态读回内存。
7. startup() (仿真起航) —— 最后的准备
这是在 m5.simulate() 被调用后,仿真循环开始前执行的最后一个函数。
作用: 激活对象。
主要任务: 调度(Schedule)第一个事件。
例如:CPU 会在此时调度它的第一个 tick 事件。如果没有对象在 startup() 里往事件队列里扔东西,仿真会瞬间结束,因为队列是空的。
Param处理
Params初始化,以vlsu为例:
Python Param → SWIG → C++ Params → XXParams.xx
VSLU.py::class VLSU::xx = Param.Int(…) → SWIG → VLSU.hh(auto generate)::VLSUParams.xx = src/python/m5/params.py::class Param.class Int(…) → SimObject
Port中的方法
对比表(Port 中常用接口)
| 方法 | 返回值 | 特性 | 时序模型 | 典型使用场景 |
|---|---|---|---|---|
sendAtomic(PacketPtr pkt) | PacketPtr / bool | 原子访问,CPU 或模块阻塞直到访问完成 | Atomic:不考虑时钟周期,访问瞬时完成 | 快速仿真,不需要 cycle 精度,建模 RTL 不必关心延时 |
sendAtomicBackdoor(PacketPtr pkt, MemBackdoorPtr &backdoor) | PacketPtr / bool | Atomic 访问 + 内存后门(backdoor) | Atomic | 用于直接访问内存内容,同时可以绕过端口发送路径,用于调试或直接读写内存 |
sendFunctional(PacketPtr pkt) const | bool | 功能性访问,非阻塞 | Functional | 仅改变或读取内存值,不触发事件,不占用时钟,通常用于初始化、快速读写或调试 |
sendMemBackdoorReq(const MemBackdoorReq &req, MemBackdoorPtr &backdoor) | bool | Backdoor 访问(绕过 cache / pipeline) | Backdoor | 用于调试或直接访问内存,不触发缓存、总线或事件,可以访问内部状态 |
sendTimingReq(PacketPtr pkt) | bool | Timing 访问请求,非阻塞 | Timing:遵循时钟与延迟,事件驱动 | 精确 cycle 模拟,建模真实时序(CPU、缓存、网络) |
tryTiming(PacketPtr pkt) const | bool | 尝试发送 Timing 请求,如果资源被占用则返回失败 | Timing | 在非阻塞状态下尝试发送请求,通常用于流水线中尝试访问共享资源 |
sendTimingSnoopResp(PacketPtr pkt) | bool | Timing snoop 响应,非阻塞 | Timing Snoop | 用于缓存一致性协议,响应其他 cache 或主存 snoop 请求 |
- Atomic(sendAtomic)
- 阻塞 CPU/模块直到访问完成。
- 不触发事件队列,速度快。
- 模型行为正确性,但不考虑 cycle。
- Functional(sendFunctional)
- 功能性访问,不影响时序。
- 直接修改或读取数据。
- 常用于初始化内存、调试工具或外部测试。
- Timing(sendTiming / tryTiming)
- 非阻塞,事件驱动,遵循 cycle 精度。
- 会考虑总线延迟、缓存延迟、争用等。
- 精确模拟真实硬件时序。
- Atomic 简单粗暴,不考虑 cycle。
- Timing 需要事件驱动调度,更接近真实硬件时序。
- Functional 不触发任何事件,直接改内存。
sendTimingReq -> TimingRequestProtocol::sendReq -> TimingResponseProtocol* ->
recvTimingReq -> handleRequest -> accessTiming -> sendResponse -> sendPacket ->
sendTimingResp -> TimingResponseProtocol::sendResp -> TimingRequestProtocol* ->
recvTimingResp -> handleRespPacket -> handleResp sendFunctional -> FunctionalRequestProtocol::send ->
recvFunctional -> accessFunctional -> makeResponse
gem5 基础概念与对应关系
| gem5 概念 | 作用 / 含义 | SystemC 对应 | Verilog 对应 | 硬件对应 | 抽象层级 |
|---|---|---|---|---|---|
| System | 顶层仿真系统对象,包含 CPU、内存、总线 | 顶层模块 SC_MODULE | 顶层 RTL module | SoC / Chip | 高抽象 |
| CPU | 模拟 CPU 核心行为 | Module + Process | Module / always / RTL | 处理器核心 | 时序抽象 / 可模拟 cycle |
| Cache | 模拟缓存行为 | Module + Signal / Process | Module + memory array | SRAM cache | 可抽象 / 可建模存储逻辑 |
| Port | 模块间通信接口(Master/Slave) | sc_port / sc_in / sc_out | input/output/inout | 总线/信号端口 | 抽象线网 |
| Packet | 请求/响应封装 | Transaction / Packet struct | N/A | Memory request / bus transaction | 抽象访问 |
| EventQueue / Event | 时钟和事件调度 | sc_event / wait | #delay, @(posedge clk) | 时钟/延迟/同步逻辑 | 抽象调度机制 |
| sendTimingReq / recvTimingReq | Timing 模型请求 / 响应 | send → MasterPort / recv → SlavePort | send → master RTL / receive → slave RTL | 总线/Cache访问 | Timing 模型抽象,非真实并行 |
| sendAtomic / sendFunctional | 非阻塞 / 阻塞内存访问 | Functional call / wait() | blocking / non-blocking assignment | Load/Store立即执行 | 高抽象 / 忽略 cycle 或事件 |
| Stats / Trace | 统计/调试信息 | N/A | N/A | 测试与验证 | 仿真辅助,软件抽象 |
| MinorCPU / O3CPU / TimingSimpleCPU | 不同 CPU 模型 | Process/Thread 模块 | RTL CPU pipeline | Pipeline / Stage / Core | 不同精度抽象 |
| SimObject | 基类,所有仿真对象继承 | SC_MODULE / object | module / struct | 逻辑单元 | 仿真抽象对象 |
DMA指令执行
在 gem5 中,执行一条 DMA 指令涉及多个组件和类的交互,主要包括 CPU、DMA 控制器、内存子系统和事件队列等。下面是一个完整的 DMA 指令执行流程示例,以说明数据流转和请求在 gem5 中经过的关键类。
假设场景
假设 CPU 发出一条 DMA 请求,要求将一段数据从内存搬运到设备(或从设备搬运到内存)。以下是 gem5 中主要涉及的类及其作用:
1. CPU 发出 DMA 请求
- 涉及类 :
X86CPU、ThreadContext - 过程 :
- CPU 代码(例如
X86CPU中)识别到 DMA 请求指令,通过ThreadContext获取当前的请求上下文(如数据地址、大小等)。 - 然后 CPU 会创建一个 DMA 请求,并将请求信息传递给 DMA 控制器。
2. DMA 控制器接收请求
- 涉及类 :
DMACtrl、DmaPort - 过程 :
-
DMACtrl类作为 DMA 控制器的主要实现类,接收到请求后,负责处理数据搬运的具体流程。 -
DMACtrl通过其DmaPort端口与系统总线或内存子系统通信,将数据请求发送到内存。 -
DMACtrl会在EventQueue中注册一个事件,设定为下一步数据搬运操作的触发时间。
3. 数据包的生成
- 涉及类 :
DmaPort、Packet - 过程 :
- 在
DmaPort::trySendTimingReq()方法中,DmaPort创建一个Packet对象,封装了 DMA 请求的数据包。 -
Packet类用于表示内存传输的实际数据块,包括地址、命令类型(读/写)和传输大小等信息。 -
DmaPort会调用sendTimingReq(pkt),将Packet发送到总线或内存控制器。
4. 发送请求到总线
- 涉及类 :
MemCtrl、Bus或SystemBus - 过程 :
-
DmaPort将Packet发送给Bus(系统总线)或MemCtrl(内存控制器)。 -
Bus负责在不同设备(如 DMA 控制器、内存、CPU 等)之间传输Packet,并确保数据请求按时送达内存控制器。
5. 内存控制器处理请求
- 涉及类 :
MemCtrl、DRAMCtrl、RubySystem - 过程 :
- 内存控制器
MemCtrl或DRAMCtrl收到 DMA 请求后,检查数据地址,判断该请求是否能够立即处理。 - 内存控制器可能会产生延迟,例如访问 DRAM 时会加入行、列访问延迟。
- 内存控制器将数据读出或写入到指定的内存地址后,通过
Packet回传结果给 DMA 控制器。
6. DMA 控制器接收数据并完成传输
- 涉及类 :
DMACtrl、DmaPort、EventQueue - 过程 :
- 当内存控制器完成数据传输后,
Packet会返回到DmaPort,然后再返回到DMACtrl。 -
DMACtrl检查是否完成所有数据块的搬运。 - 如果数据搬运尚未完成,
DMACtrl会继续在EventQueue中注册新的事件,并在未来的Tick触发下一次数据块的传输。 - 当所有数据传输完成后,
DMACtrl将通过EventQueue向 CPU 发送一个完成通知(通常是中断),告知 CPU DMA 操作已完成。
7. CPU 处理 DMA 完成通知
- 涉及类 :
X86CPU、Interrupts - 过程 :
- CPU 通过中断子系统
Interrupts接收到 DMA 完成的中断通知。 - CPU 的中断处理程序开始运行,确认 DMA 操作已完成,并处理后续操作(如读取搬运的数据或继续执行程序)。
流程总结
- 主要数据流 :
CPU→DMACtrl→DmaPort→Bus→MemCtrl→DmaPort→DMACtrl→CPU - 关键类 :
-
DMACtrl:管理 DMA 操作的控制器。 -
DmaPort:DMA 控制器与其他组件通信的端口。 -
Packet:封装数据传输请求和响应的数据包。 -
MemCtrl:内存控制器,处理实际的读/写请求。 -
Bus:连接 DMA 控制器和内存控制器的总线。 -
EventQueue:调度 DMA 控制器和其他组件之间的事件,确保各组件按时间顺序工作。
通过上述机制,gem5 实现了 DMA 指令的异步数据流转和各组件间的精确同步,所有操作在单线程中按事件驱动执行。
CPU / MasterPort
│
│ sendTimingReq(pkt)
▼
SlavePort (Cache / Memory)
│
│ recvTimingReq(pkt) ←── 处理请求
│ │
│ └─> 处理完毕后,生成响应 pkt
│
│ sendTimingResp(pkt)
▼
MasterPort / CPU
│
│ recvTimingResp(pkt) ←── 收到响应
▼
CPU / 上层模块处理数据
MasterPort 发起请求 → 调用 sendTimingReq()
SlavePort 接收请求 → recvTimingReq()
SlavePort 处理请求 → 内部事件 process()
处理完成生成响应 → 调用 sendTimingResp()
MasterPort 接收响应 → recvTimingResp() → 上层 CPU / 模块更新寄存器或状态
Pipeline
Fetch1
这是 MinorCPU 里取指一阶段(Fetch1)的主逻辑。它每个周期被调用一次,完成分支重定向判断、为下游预留槽位、发起取指请求、推进 icache 事务队列、接收并输出取回的指令行。按代码走一遍:
它先读入来自其他阶段的“控制线”
-
execute_branch = *inp.outputWire:来自 Execute(后段)的分支/流切换信息(真实执行得到的分支/异常/redirect)。 -
fetch2_branch = *prediction.outputWire:来自 Fetch2 的分支预测结果(前段预测)。 -
line_out = *out.inputWire:当前要输出给下游(Fetch2)的取指行(初始应是 bubble)。
1) 标记可否向下游发(是否被下游堵住)
for each tid:
fetchInfo[tid].blocked = !nextStageReserve[tid].canReserve();
- 每个线程各有一个“下游槽位预留器”。不能预留就视为被阻塞。
2) 处理分支与“流切换(changeStream)”
按线程维度处理 Execute 与 Fetch2 预测 两条分支线:
- 如果两条分支都有效且指向同一线程:
- 优先响应 Execute(真实)分支:若
isStreamChange()且线程未 Halt,则changeStream(execute_branch)。 - 若同时有预测分支,打印“忽略预测”的调试信息(真实分支优先)。
- 否则,若真实分支不需要切流、但预测需要,且预测的
newStreamSeqNum与线程当前streamSeqNum匹配,则执行changeStream(fetch2_branch)。
- 优先响应 Execute(真实)分支:若
- 如果来自不同线程:
- 对各自线程分别判断是否需要
changeStream(...)(同样要考虑线程是否 Halt、以及预测的 streamSeqNum 是否匹配)。
- 对各自线程分别判断是否需要
这里的 streamSeqNum 是“取指流代号”,用来丢弃旧流上的迟到请求/响应,避免乱序/陈旧数据污染。
3) 若在飞取指次数未达上限,选择一个线程发起取指
if (numInFlightFetches() < fetchLimit) {
tid = getScheduledThread();
if (tid != Invalid) {
fetchLine(tid); // 生成并发出一个 I-cache 行取指请求
nextStageReserve[tid].reserve();// 下游预留一个槽位
}
}
-
fetchLimit:控制最多挂起多少条取指。 -
getScheduledThread():选择下一个要取指的 SMT 线程(调度策略)。 -
fetchLine(tid):根据当前 PC/流状态构造取指行请求(I-cache miss 则走访存)。 - 成功发出后为该线程在下游队列预留位置(避免过度生产)。
4) 推进端口/队列:让在飞的请求继续流动
stepQueues();
- 驱动 icachePort 的发送/返回队列,与内存系统继续握手(把 outstanding 的事件推进一拍)。
5) 若有已完成的取指行(transfer),消费它
if (!transfers.empty() && transfers.front()->isComplete()) {
auto response = transfers.front();
if (response->isDiscardable()) {
nextStageReserve[tid].freeReservation(); // 释放之前预留的槽位
cpu.wakeupOnEvent(Pipeline::Fetch1StageId); // 提醒下一拍重试
} else {
processResponse(response, line_out); // 把取回的指令行打到 line_out 输出
}
popAndDiscard(transfers); // 出队
}
- 完成的响应若 属于旧的流(discardable),直接丢弃并释放预留槽位;否则调用
processResponse生成对下游的输出。 - 这与前面的
streamSeqNum机制配合,保证 redirect 后旧流迟到数据被丢弃。
6) 若本拍产生了输出,记录流水线活动
if (!line_out.isBubble())
cpu.activityRecorder->activity();
- 标记该阶段“有活”,利于节能/统计。
小结(做了哪些事)
- 读分支线(真实/预测),必要时切换取指流(带 streamSeqNum 防旧流污染)。
- 看下游是否能接,并在 in-flight 限制内选择线程发起取指、为下游预留槽位。
- 推进 I-cache/内存队列,接收已完成的取指行:
- 属于旧流 → 丢弃并释放预留;
- 属于当前流 → 产出 line_out 给 Fetch2。
- 记录活动以推进流水线。
这就是 MinorCPU 取指一级(Fetch1)的“发/收/重定向/背压/过滤陈旧响应”的完整工作流。
Fetch2
这是 MinorCPU 的取指第二阶段(Fetch2)主循环。它把 Fetch1 送来的“指令行”分解成具体指令、做本地分支预测、并把解出的动态指令打包给下游。按代码路径分解如下:
它先做的输入整理
- 把来自 Fetch1 的一条
ForwardLineData(若非 bubble)塞进对应线程的输入环形缓冲:inputBuffer[tid].setTail(...)。 - 取本阶段的输出句柄
insts_out(起始是 bubble),以及两路分支控制:-
branch_inp:来自 Execute 的真实分支/redirect; -
prediction:本阶段将要产生并输出给 Fetch1 的预测分支。
-
1) 先处理来自 Execute 的真实分支
-
updateBranchPrediction(branch_inp):用真实分支结果训练/更新本地预测结构。 - 若
branch_inp.isStreamChange()(发生真实流切换),丢弃该线程所有输入行并将havePC=false(等待新 PC 重新对齐)。
2) 清理“预测序列号不匹配”的陈旧行
- 对每个线程:
- 标记下游是否能接收:
thread.blocked = !nextStageReserve[tid].canReserve() - 从输入缓冲看顶端行
line_in,在流序号(streamSeqNum)一致但预测序号(predictionSeqNum)不一致时,循环丢弃这些行,并清havePC=false。(保证 redirect 或重预测后,旧预测生成的行不会被错误解出。)
- 标记下游是否能接收:
3) 选择一个线程做本拍取指解码
-
tid = getScheduledThread():选择一个可用线程(SMT 调度策略)。 - 若有可用线程,进入主循环把输入行“解成指令”,直到:
- 当前输入行用完 / 需要更多字节但没行 / 已达本拍输出上限
outputWidth/ 已产生一个分支预测prediction
- 当前输入行用完 / 需要更多字节但没行 / 已达本拍输出上限
在这段循环里做的事
- 取该线程的
decoder与fetch_info(PC/序列号/行内偏移等状态)。 - 必要时设置 PC 与行内偏移:
- 若切换了流或此前没有 PC:根据
line_in->pc和解码器的pcMask()计算inputIndex(该 PC 在本行中的字节偏移),设置fetch_info.pc,decoder->reset()。
- 若切换了流或此前没有 PC:根据
- 判断是否需要丢弃整行(同流但预测序号不匹配);
- 若
line_in->isFault():构造一条 fault dyn_inst(携带 fault),并继续允许生成其他指令。 - 否则执行解码:
- 把当前行字节拷给解码器
moreBytesPtr();若不够,调用decoder->moreBytes(...)。 - 如果
instReady():-
decoded_inst = decoder->decode(*pc),构造MinorDynInst,设置fetchSeqNum/predictionSeqNum,拷贝 PC; - 统计指令类型(load/store/atomic/vector/fp/int);
- 微 PC 处理:
pc->uReset(),确保随后的advancePC从宏指令起点计; -
decoded_inst->advancePC(*pc):推进到下一条; -
predictBranch(dyn_inst, prediction):对该指令做分支预测,必要时填充prediction(一旦产生预测,本拍停止再取)。
-
- 如果解码器还需要更多字节,按
moreBytesSize()推进inputIndex。
- 把当前行字节拷给解码器
- 若本条产生了
dyn_inst:-
fetchSeqNum++;若这是本拍第一条,insts_out.resize(outputWidth); - 把
dyn_inst放到insts_out.insts[output_index++];必要时输出 MinorTrace。
-
- 维护流变更跟踪:
lastStreamSeqNum = line_in->id.streamSeqNum。 - 行管理与丢弃策略:
- 如果已产生
prediction或本行是 fault:丢弃所有输入,havePC=false; - 若只是“预测序号不匹配导致丢行”:仅弹出该行,
havePC=false; - 若
inputIndex == lineWidth:本行解析完,弹出该行(PC 保留,防行跨界的指令)。
- 如果已产生
- 若允许跨多行处理(
processMoreThanOneInput),空行时会继续取下一行循环。
4) 输出与资源预留
- 始终把本阶段的预测结果送出:
predictionOut.inputWire = prediction(可能是 bubble)。 - 如果本拍生成了任何指令(
!insts_out.isBubble()):- 标记活动:
cpu.activityRecorder->activity(); - 设置
insts_out.threadId = tid; - 在下游预留一个槽位:
nextStageReserve[tid].reserve()。
- 标记活动:
- 若仍有可处理的输入且下游能接,激活本阶段:
activateStage(Fetch2StageId)。 - 把刚塞入的输入行推尾到缓冲:
inputBuffer[tid].pushTail()(确保缓冲前进)。
这一阶段“做了哪些事”的要点清单
- 训练预测器:用后段真实分支结果更新本地 BP。
- 响应真实 redirect:来真实分支就清空输入、等待新流 PC。
- 清理陈旧预测行:在相同流号下,丢弃预测序号不匹配的行。
- 选线程并解码:从输入行按 PC 偏移喂解码器,生成
MinorDynInst。 - 产生分支预测:对解出的指令调用
predictBranch,一旦预测,终止本拍继续解。 - 故障行处理:fault 行包装为 fault 指令输出。
- 行/PC 维护:处理跨行、行尾弹出、流号跟踪、微 PC 复位。
- 统计与输出:记录基本指令类别,打包到
insts_out,给下游预留槽位并标记活动。
它与 Fetch1 配合:Fetch1 负责取行与流切换,Fetch2 负责解码成指令与预测下一跳,并用 streamSeqNum/predictionSeqNum 机制保证 redirect 后的陈旧数据不外泄。
Decode
这是 MinorCPU 的 Decode 阶段主循环。它把 Fetch2 产出的“动态指令包”进一步微指令分解(macro→micro)、分配执行序号、并把结果推给下游。按执行顺序说明:
输入与阻塞探测
- 若来自上游
inp不是 bubble,则按线程把该批ForwardInstData放入对应inputBuffer[tid]的尾部(setTail)。 - 取到本阶段输出
insts_out(初始是 bubble)。 - 为每个线程标记是否被下游阻塞:
decodeInfo[tid].blocked = !nextStageReserve[tid].canReserve()。 - 选择一个可执行线程:
tid = getScheduledThread()(SMT 选择策略)。
取指令并解码/分解
若选到线程:
-
insts_in = getInput(tid)取该线程输入批;output_index计数输出槽位。 - 循环装填输出,条件:仍有输入元素、未填满
outputWidth。 - 逐条查看
inst = insts_in->insts[inputIndex]:- bubble:跳过,
inputIndex++,并清inMacroop=false。 - fault 指令:只是“传递”错误,不分解;
inputIndex++,清inMacroop。 - 非 fault:
- 取
static_inst = inst->staticInst,默认output_inst = inst。 - 如果是 macro-op(需要拆微指令):
- 若刚进入一个宏指令,初始化
microopPC = inst->pc,置inMacroop=true。 - 通过
static_inst->fetchMicroop(microopPC->microPC())取一条 micro-op 的 StaticInst。 - 构造新的
MinorDynInst output_inst(携带原 id/流信息),设其pc = microopPC。 - 只有在 最后一条 micro-op 时,才把 分支预测位与目标(
predictedTaken/Target)从宏指令继承到该 micro-op(避免在中间 micro-op 上带预测)。 -
static_micro_inst->advancePC(*microopPC)前进微 PC。 - 若已是最后一条 micro-op:
inputIndex++并inMacroop=false(完成对该宏指令的分解)。
- 若刚进入一个宏指令,初始化
- 如果 不是 macro-op:不分解,直接“传递”该动态指令;
inputIndex++,inMacroop=false。 - 统一处理:
- 赋执行序号:
output_inst->id.execSeqNum = decode_info.execSeqNum; decode_info.execSeqNum++。 - 若是本批第一条,
insts_out.resize(outputWidth)以便写入。 - 写入
insts_out.insts[output_index++] = output_inst。
- 赋执行序号:
- 取
- bubble:跳过,
- 批内输入耗尽时(
inputIndex == insts_in->width()):- 断言不在宏指令中(
!inMacroop),弹出该输入批popInput(tid); - 若允许一次处理多批(
processMoreThanOneInput),则“wrap”再取下一批继续。
- 断言不在宏指令中(
输出、活跃与推进
- 若本拍确实产生了输出(
!insts_out.isBubble()):- 记录流水线活动
activity(); - 标注输出所属线程
insts_out.threadId = tid; - 为下游预留一个槽位:
nextStageReserve[tid].reserve()。
- 记录流水线活动
- 若还有未处理的输入且下游能接收,激活本阶段:
activateStage(Pipeline::DecodeStageId)(提示下一拍继续干活)。 - 最后,把刚放到尾部的输入批真正入队:
inputBuffer[tid].pushTail()(确保队列推进)。
这个阶段的“关键职责”小结
- 输入批到线程缓冲:把 Fetch2 的批次按线程入队。
- SMT 选择 + 背压:基于
nextStageReserve感知下游容量,选择活跃线程。 - 宏指令分解:把 macro-op 拆成若干 micro-ops;仅在最后一个 micro-op上传递分支预测信息。
- 执行序号编排:为每条输出指令分配单调递增的
execSeqNum。 - 批内迭代与多批包容:在不阻塞且未达
outputWidth前,尽可能用完输入;支持一次吞多批。 - 产出与资源预留:写
insts_out,为下游预留槽位并记录活动,维持流水线流动性。
Execute
这是 MinorCPU 的 Execute 阶段主循环。它在每拍里完成:把上游指令入队、检查中断、提交(Commit)、发射(Issue)、推进功能单元与 LSQ、生成分支/停取指控制、决定是否下拍继续唤醒。按代码顺序梳理:
入口与输入
- 若上游
inp不是 bubble,则按线程把该批ForwardInstData放入本阶段inputBuffer[tid]尾部(setTail)。 - 准备本阶段输出
BranchData &branch(初始是 bubble)。 -
num_issued=0统计本拍发射条数。
1) 先推进内存侧(避免背压)
-
lsq.step();:推进数据缓存端口/LSQ 状态,可能释放 LSQ 请求队列的空间。
2) 处理中断优先
-
checkInterrupts(branch, interrupted):- 如有待处理的中断,返回
interrupt_tid并在branch中填入中断引发的控制流变化;本拍不再对其它线程发射/提交。
- 如有待处理的中断,返回
- 若没有中断,但
branch早已有旧分支(非 bubble),本拍让出一拍以便该分支完成(打印 DPRINTF 并跳过发射/提交)。
3) 正常提交与发射
- 选择可提交线程:
commit_tid = getCommittingThread()若存在:
- 取其上下文
commit_info,按 drain 状态分三种:-
DrainCurrentInst:只提交当前宏指令内的 micro-ops(不杀后续),若提交后“处于指令间隙”,切到DrainHaltFetch;丢弃本拍生成的分支branch=bubble()。 -
DrainAllInsts:清空该线程所有输入(popInput直到空),然后执行一次commit(commit_tid, /*only_microops=*/false, /*kill_all=*/true, branch)。 -
NotDraining:若处于中断且该线程仍有中断挂起,则只提交 micro-ops;否则正常提交(commit(..., only_commit_microops, false, branch))。
-
- 若刚进入
DrainHaltFetch:构造一个 HaltFetch 分支更新(updateBranchData(..., BranchData::HaltFetch, ...)),唤醒执行阶段并把 drain 状态切到DrainAllInsts(等待清空)。
- 取其上下文
- 选择可发射线程:
issue_tid = getIssuingThread()若存在:
num_issued = issue(issue_tid);(即使有中断挂起也允许发射,本着“之后中断会改变流”的假设)。
4) 推进功能单元流水并评估下一拍是否仍需活跃
- 收集各线程“下一条可发射指令”到
next_issuable_insts,初步标志can_issue_next=false。 - 遍历所有功能单元流水线:
-
pipeline->advance();推进一拍。 - 若流水线占用且未阻塞,
becoming_stalled=false(说明还有活可以动)。 - 试投下一条:若流水线不堵、类型匹配(
provides(opClass))、scoreboard 允许、时序/资源可行且canInstIssue(...),则can_issue_next=true。
-
- 检查在飞指令队列头是否可被提交,若任一线程
inFlightInsts非空,则FUs_need_to_update=true。
5) 计算是否需要唤醒下一拍
- 组合条件:
need_to_tick = (本拍发射过) || (功能单元仍能前进) || (下一拍可发射) || (可能可提交) || (LSQ 需要前进) || (有中断在处理) - 若
need_to_tick,调用cpu.wakeupOnEvent(Pipeline::ExecuteStageId)约定下一拍继续调度本阶段。
6) 活动标记与推进输入游标
- 若本拍
branch非 bubble(产生了控制流更新,如中断/Drain/HaltFetch/分支),记一次流水线活动:cpu.activityRecorder->activity()。 - 若入口有输入(非 bubble),将之前
setTail的项真正推入队列:inputBuffer[tid].pushTail()。
总结:本函数一拍内完成的工作
- LSQ/内存侧推进,先消背压。
- 中断最高优先级:生成
branch,暂停其它线程提交/发射。 - 提交阶段:按 drain 状态/中断状态选择仅提交 micro-ops、清空或正常提交;必要时发出 HaltFetch 控制。
- 发射阶段:从可发射线程取指令给功能单元。
- 功能单元推进与活跃性判断:前进流水、评估是否还有可发射/可提交/LSQ 需要推进,从而决定是否唤醒下一拍。
- 输出与 bookkeeping:如有分支/控制输出,记录活动;推进输入缓冲。
🧩 逻辑组成与顺序
按执行顺序,Execute 阶段每拍会依次完成以下逻辑块:
① 前期维护与输入处理
- 把上游 Decode 阶段输出的指令包放入本阶段
inputBuffer; - 调用
lsq.step()推进 Load/Store Queue(模拟访存端口进展),释放请求槽位; - 初始化计数变量与输出分支
branch。
② 中断检查(Interrupt check)
- 调用
checkInterrupts(branch, interrupted);- 若有中断触发,则:
- 生成
BranchData(表示“跳转到中断入口”); - 暂停本拍后续发射与提交;
- 优先处理中断线程(其它线程不动)。
- 生成
- 若有中断触发,则:
🔹这一块逻辑相当于: 在 issue/commit 前先做异常检测 → 对应乱序 CPU 的“interrupt flush + redirect”。
③ Commit 阶段逻辑
- 选中一个可提交线程:
commit_tid = getCommittingThread(); - 调用
commit(commit_tid, ...)完成提交操作; - 根据
drainState(CPU 清空状态)分支:-
DrainCurrentInst: 只提交当前宏指令中的 micro-ops; -
DrainAllInsts: 清空整个线程的在飞指令; -
NotDraining: 正常提交(含分支、异常、访存完成的指令)。
-
- 若
DrainHaltFetch状态出现,发出HaltFetch分支更新信号给取指阶段(阻止后续 Fetch)。
🔹对应传统流水线的 Commit/Writeback 阶段。MinorCPU 没有独立的 Reorder Buffer,而是顺序地在 Execute 中调用 commit 来实现。
④ Issue 阶段逻辑
- 选出可发射线程:
issue_tid = getIssuingThread(); - 调用
issue(issue_tid):- 检查该线程最前端指令依赖、资源、scoreboard;
- 若可发射,则将其送入相应功能单元(ALU/FPU/LoadStore 等);
- 计数
num_issued++。
🔹这对应经典流水线的 Issue + Dispatch + Execute 准备阶段。
⑤ 功能单元执行与调度推进
- 遍历所有功能单元(
funcUnits)与其内部 pipeline:- 调用
pipeline->advance()推进执行; - 若 pipeline 中仍有占用但未阻塞,则记录本拍不“stalled”;
- 检查是否能在下一拍发射更多指令(预测
can_issue_next)。
- 调用
- 检查在飞指令(inFlightInsts)是否仍需更新 → 影响是否保持活动。
🔹这相当于多周期 Execute 阶段推进,带延迟执行、功能单元 busy 状态。
⑥ 活动检测与唤醒机制
- 综合:
- 本拍是否发射过;
- 功能单元是否仍可前进;
- 是否有下一拍可发射;
- 是否有在飞指令待提交;
- LSQ 是否需要继续推进;
- 是否有未处理中断。
- 若任一为真,则
cpu.wakeupOnEvent(Pipeline::ExecuteStageId)保证下一拍继续调度。
🔹这对应“流水线活动保持”机制。若完全空闲(stall 且无事件),则进入休眠,直到上游或中断唤醒。
⑦ 输出与收尾
- 若本拍生成分支(
branch != bubble()),标记活动; - 推进输入队列尾指针(
inputBuffer.pushTail())。
🧠 总结:阶段融合顺序
| 执行阶段子功能 | 执行顺序 | 对应传统流水阶段 | 关键函数 |
|---|---|---|---|
| 1️⃣ 输入入队 + LSQ 前进 | 最先 | Memory / Pre-issue | lsq.step() |
| 2️⃣ 中断检测 | 早期 | Exception / Flush | checkInterrupts() |
| 3️⃣ Commit 提交 | 其次 | Writeback / Retire | commit() |
| 4️⃣ Issue 发射 | 随后 | Issue / Dispatch | issue() |
| 5️⃣ 功能单元推进 | 后半 | Execute | pipeline->advance() |
| 6️⃣ 活动检测与唤醒 | 收尾 | Pipeline control | wakeupOnEvent() |
| 7️⃣ 输出分支与清理 | 最后 | Branch / Control | updateBranchData() |
所以:
✅ MinorCPU 的 Execute 阶段融合了传统 CPU 的 issue → execute → commit 流程。顺序大致是:
LSQ step → 中断检测 → commit → issue → 功能单元推进 → 活动检测 → 输出分支
与官方MinorCPU差别
✅ 当前版本(你贴的代码)中的行为,和
🕹️ “传统的” MinorCPU(gem5 官方默认的 Minor 模型)到底有何区别。
1. 先说结论
🔹 你看到的这份 Execute 实现,并不是乱序执行(out-of-order)。它依然是顺序执行(in-order),但:
- 它允许多个线程并发;
- 它能让功能单元有流水(multi-cycle execution),看起来像乱序;
- 它引入了 “scoreboard + pipeline 资源检查”,形成“有限乱序”的效果。
换句话说,它是 “顺序发射 + 异步完成”,而不是传统意义上的乱序执行。
2. “传统的” MinorCPU Execute 阶段代码结构
在 gem5 官方仓库(例如 src/cpu/minor/execute.cc)中,
经典的 MinorCPU 的 Execute::evaluate() 大致逻辑如下(简化伪码):
Execute::evaluate() {
// 1. 从 Decode 阶段取输入
if (!inp.outputWire->isBubble())
inputBuffer[tid].setTail(*inp.outputWire);
BranchData &branch = *out.inputWire;
// 2. 处理中断(如果有)
checkInterrupts(branch);
// 3. 选出可提交线程,执行 commit()
ThreadID commit_tid = getCommittingThread();
if (commit_tid != InvalidThreadID)
commit(commit_tid, ...);
// 4. 选出可发射线程,执行 issue()
ThreadID issue_tid = getIssuingThread();
if (issue_tid != InvalidThreadID)
issue(issue_tid);
// 5. 推进功能单元(模拟执行过程)
for (auto fu : funcUnits)
for (auto &pipeline : fu->pipelines)
pipeline->advance();
// 6. 若仍有未完成指令或中断待处理,则唤醒下一拍
if (stillActive())
cpu.wakeupOnEvent(Pipeline::ExecuteStageId);
}
特点:
| 项目 | 说明 |
|---|---|
| 顺序执行模型 | 所有线程共用一个 issue/commit 时隙,保证顺序。 |
| 功能单元流水化 | 功能单元可以多拍完成指令,但同一线程指令不能乱序。 |
| scoreboard 控制 | 检查寄存器、功能单元占用冲突,确保依赖有序。 |
| 无重排序缓冲区(ROB) | 不支持真正的乱序提交或投机执行。 |
| 多线程切换 | 可以多线程交替 issue(时间片式多线程)。 |
所以传统 Minor 模型其实是:“顺序发射、乱序完成、顺序提交”
——这是 in-order pipeline + pipelined FU 的典型特征。
3. 你贴的版本的不同之处
从你提供的 Execute::evaluate() 代码看,它是经过扩展/优化版的 MinorCPU,主要区别如下:
| 对比点 | 传统 MinorCPU | 你贴的版本 |
|---|---|---|
| 线程模型 | 1~2 个线程,轮询发射 | 支持多线程循环检查,每个线程独立 commit/issue |
| issue/commit 次序 | 固定顺序:commit → issue | 同样顺序,但对每个线程独立处理 |
| 功能单元控制 | 简单 pipeline + advance() | 支持多 pipeline + canInstIssue() 动态决策 |
| scoreboard | 全局寄存器依赖表 | 每线程独立 scoreboard,允许更并行发射 |
| 发射逻辑 | 仅一个线程可 issue | 可从多个线程尝试 issue(但每线程仍顺序) |
| 乱序程度 | 完全顺序执行 | 支持“跨线程并发、功能单元异步完成”——伪乱序 |
也就是说,它仍然是 Minor 模型(in-order core),
但行为更接近 Fine-grained multithreading (FGMT),即“细粒度多线程顺序核”。
4. 为什么看起来像“乱序执行”
有三个机制让它表现得像乱序:
| 机制 | 效果 |
|---|---|
功能单元多周期 (pipeline->advance()) | 指令执行周期不同,完成顺序可能不同。 |
| 多线程发射 | 多线程并发执行,不同线程的指令交错。 |
| scoreboard 控制 | 允许无依赖的指令抢先发射(但同线程仍按序)。 |
所以在 trace 里你可能看到:
Thread 0: inst 1 执行
Thread 1: inst 1 执行
Thread 0: inst 2 等待
Thread 1: inst 2 提交
这看起来像乱序,其实是“线程交错 + 异步执行”的假象。
5. 真正乱序(OoO)CPU 的区别
真正的乱序核(如 gem5 的 O3CPU)包含:
| 模块 | 功能 |
|---|---|
| 重命名寄存器 (Register Rename) | 消除写后读/写后写依赖 |
| 指令窗口 / 调度队列 | 按准备好时间动态 issue |
| ROB(Reorder Buffer) | 按序提交结果 |
| 投机执行 / 分支恢复 | 支持预测执行、回滚恢复 |
MinorCPU 没有这些,而只是:
- 检查寄存器占用;
- 顺序发射;
- 允许多周期执行。
所以严格来说:
🟢 Minor 是 “顺序执行(in-order) + 异步执行单元”🔴 O3CPU 才是 “真正乱序(out-of-order)”。
总结一句话
| 项目 | 传统 MinorCPU | 你贴的版本 | 真正 OoO CPU |
|---|---|---|---|
| 发射顺序 | 顺序 | 顺序(跨线程并行) | 乱序 |
| 提交顺序 | 顺序 | 顺序 | 顺序(通过 ROB) |
| 线程并行 | 单线程/双线程 | 多线程独立 issue | 多线程支持但复杂 |
| 执行单元 | 多周期顺序 | 多 pipeline 异步 | 动态调度 |
| 是否乱序 | ❌ 否 | ⚠️ 类乱序(异步+多线程) | ✅ 是 |